<?php
/**
 * ORM(对象映射模型) 数据应用层
 * 把数据模型映射成对象来操作数据。
 *
 * @author Mo yi <root@imoi.cn>
 * @link   http://www.imoi.cn
 * @copyright Copyright &copy; 2010-2012 DuoLam Software LLC
 * @license http://www.imoi.cn/license/
 * @version $Id: dpActiveRecord.php 2799 2012-04-03 16:09:13Z Mo.yi $
 * @package db.ar
 * @since 1.0
 */

class dpActiveRecord extends dpComponent
{
	/**
	 * 数据库连接句柄
	 * @var object
	 */
	public $db;

	/**
	 * 数据模型，存放本身
	 * @var array
	 */
	private static $_models = array();
	
	/**
	 * 数据库架构驱动
	 */
	private $_s;	

	/**
	 * 上一次操作的主键值
	 */
	private $_pk;

	/**
	 * 数据属性
	 */
	private $_attributes = array();

	/**
	 * 是否是新记录
	 */
	private $_new = false;

	/**
	 * 表别名
	 */
	private $_alias = 's';

	/**
	 * meta
	 */
	private $_meta = array();

	/**
	 * 最后执行的SQL
	 */
	private $_sql;

	/**
	 * 获取数据接口
	 */
	public  $finder;

	/**
	 * 查询范围
	 */
	public  $scopesProcess = array();

	/**
	 * 关联器
	 */
	public  $with = array();

	/**
	 * 构造方法
	 */
	public function __construct()
	{
		$this->setIsNew(true);
		$this->init();
		$this->connect();
	}

	/**
	 * 自定义初始化功能方法
	 * 重载该方式实现初始化操作
	 */
	public function init(){}

	/**
	 * 返回表别名
	 */
	public function getTableAlias()
	{
		return $this->_alias;
	}

	/**
	 * 设置表别名，默认为s
	 * @param string $alias
	 */
	public function setTableAlias($alias)
	{
		$this->_alias = $alias;
	}

	/**
	 * 返回表名称，没有设置的话，默认把类名当数据表名
	 * @return string 数据表名称
	 */
	public function tableName()
	{
		return get_class($this);
	}

	/**
	 * 返回连接类型，没有设置的话，默认连接主库
	 * @return string 连接名称
	 */
	public function linkName()
	{
		return 'master';
	}

	/**
	 * 设置新记录状态
	 */
	public function setIsNew($status)
	{
		$this->_new = $status;
	}

	/**
	 * 返回记录状态
	 */
	public function getIsNew()
	{
		return $this->_new;
	}

	/**
	 * 设置数据缓存时间
	 */
	public function setCache($time = -1)
	{
		$this->db->setCache($time);
		return $this;
	}

	/**
	 * 设置主键
	 */
	public function primaryKey()
	{
		return false;
	}

	/**
	 * 连接数据库
	 */
	protected function connect()
	{
		$this->db = D::app()->db();
		$this->db->resetLink($this->linkName());
		$this->db->createSql();
		$this->db->resetTable($this->tableName());
		$this->_s = $this->db->getSchema();
		$this->finder = new dpActiveFinder($this, $this->_s);
		$this->initMeta();
	}

	/**
	 * 返回数据模型，指定名字，则返回指定的数据库模型。
	 * 如果指定的模型不存在，则报错
	 * @param string $className 模型名称
	 * @return object 数据模型对象
	 */
	public static function model($className = __CLASS__)
	{
		if (isset(self::$_models[$className]))
			return self::$_models[$className];
		else
			return self::$_models[$className] = new $className(null);
	}

	/**
	 * 初始化表结构信息
	 */
	protected function initMeta()
	{
		$this->_meta = $this->_s->getColumn($this->tableName());
	}

	/**
	 * 获取表结构信息
	 * @return array
	 */
	public function getMeta()
	{
		return $this->_meta;
	}

	/**
	 * 获取表中字段名
	 * @return array
	 */
	public function getColumnName()
	{
		if (is_array($this->_meta)) {
			$meta = array();
			foreach ($this->_meta as $v) {
				$meta[] = $v['name'];
			}
		}
		return $meta;
	}

	/**
	 * 设置映射表字段的数据集，有填充空属性字段功能
	 * @param array $data 数据集
	 * @return object $this 类本身
	 */
	public function setData($data)
	{

		$fillingValue = array
		(
			'datetime' => date('Y-m-d H:i:s'),
			'date'     => date('Y-m-d'),
			'time'     => date('H:i:s')
		);

		foreach ($this->_meta as $k => $v) {
			if (array_key_exists($v['type'],$fillingValue))
				$default = $fillingValue[$v['type']];
			else
				$default = empty($v['default']) ? null : $v['default'];
			if (isset($data[$v['name']])) {
				$value = $this->db->current->escape($data[$v['name']]);
				$this->setAttribute($v['name'],$this->db->current->unEscape($value));
			} else if (isset($v['pk']) && $v['auto']) {
				continue;
			}
		}
		return $this;
	}

	/**
	 * 获取当前映射字段的属性数据
	 * @return array 数据集
	 */
	public function getData()
	{
		return $this->_attributes;
	}

	/**
	 * 设置属性值
	 * @param string $name 	属性名
	 * @param void   $value 属性值
	 */
	public function setAttribute($name, $value)
	{
		if (property_exists($this,$name))
			$this->$name = $value;
		else if (in_array($name, $this->getColumnName()))
			$this->_attributes[$name] = $value;
		else
			return false;
		return true;
	}

	/**
	 * 获取属性值
	 * @param string $name 属性名
	 */
	public function getAttribute($name)
	{
		if (property_exists($this,$name))
			return $this->$name;
		else if (isset($this->_attributes[$name]))
			return $this->_attributes[$name];
	}

	/**
	 * 对需要验证的数据进行规则有效检查
	 * @return boolean or string
	 */
	public function valid()
	{

	}

	/**
	 * 数据规则验证，继承该类后，实现该方法，需要返回一个数组
	 *
	 * 对update insert类方法传进的数据规则进行一个安全检查
	 * 如果不通过验证，返回提示错误信息，如果通过验证，返回true
	 * 规则分为内置规则和自定义规则，规则支持正则表达式
	 * 内置规则：
	 * required				如果元素为空，则返回FALSE	 
	 * is_unique			如果表单元素的值与指定数据表栏位有重复，则返回False（注：比如is_unique[User.Email]，那么验证类会去查找User表中Email栏位有没有与表单元素一样的值，如存重复，则返回false，这样开发者就不必另写Callback验证代码。）	is_unique[table.field]
	 * min_length			如果表单元素值的字符长度少于参数中定义的数字，则返回FALSE	min_length:6
	 * max_length			如果表单元素值的字符长度大于参数中定义的数字，则返回FALSE	max_length:12
	 * exact_length			如果表单元素值的字符长度与参数中定义的数字不符，则返回FALSE	exact_length:8
	 * greater_than			如果表单元素值是非数字类型，或小于参数定义的值，则返回FALSE	greater_than:8
	 * less_than			如果表单元素值是非数字类型，或大于参数定义的值，则返回FALSE	less_than:8
	 * alpha				如果表单元素值中包含除字母以外的其他字符，则返回FALSE	 
	 * alpha_numeric		如果表单元素值中包含除字母和数字以外的其他字符，则返回FALSE	 
	 * alpha_dash			如果表单元素值中包含除字母/数字/下划线/破折号以外的其他字符，则返回FALSE	 
	 * numeric				如果表单元素值中包含除数字以外的字符，则返回 FALSE	 
	 * integer				如果表单元素中包含除整数以外的字符，则返回FALSE	 
	 * decimal				如果表单元素中输入（非小数）不完整的值，则返回FALSE	 
	 * is_natural			如果表单元素值中包含了非自然数的其他数值 （其他数值不包括零），则返回FALSE。自然数形如：0,1,2,3....等等。	 
	 * is_natural_no_zero	如果表单元素值包含了非自然数的其他数值 （其他数值包括零），则返回FALSE。非零的自然数：1,2,3.....等等。	 
	 * valid_email			如果表单元素值包含不合法的email地址，则返回FALSE	 
	 * valid_ip				如果表单元素的值不是一个合法的IP地址，则返回FALSE。
	 * match				如果表单元素的值不符合第二参数正则，则返回FALSE	matches[form_item]
	 */
	public function rules()
	{
		return array();
	}

	/**
	 * ORM关联查询，设置当前A模型关联B模型，进行数据查询。。
	 * 需要注意的是，现版本仅以当前A模型关联其他模型，不支持A关联着B模型，B模型又去关联C模型或者其他模型。
	 * 但可以A模型关联B模型，又关联C模型....又关联N模型。
	 * 继承本类后，实现该方法，需要返回一个数组
	 *
	 * 数组格式：array(
	 *				'关联名'=>array(
	 *					'关联模型名',
	 *					'A模型外键ID',
	 *					'B模型关联ID',
	 *					'设置B模型要取的字段，默认为*',
	 *					'WHERE查询条件，也可使用查询范围设置查询条件',
	 *					'JOIN类型，默认OUTER JOIN，支持left right inner full'
	 *				)
	 *			);
	 * 设置取的字段和条件可以为空，关联模型名和关联ID必填
	 * @example 
	 * return array( 'rela_user_info' => array('userModel', 'user_id'));
	 * 调用方法： xxxModel::model()->rela_user_info()->finds();
	 *
	 * 当前ORM关联版本：1.0
	 * @return array
	 */
	public function relations()
	{
		return array();
	}

	/**
	 * 查询范围 继承该类后，实现该方法，需要返回一个数组
	 *
	 * 支持 condition(条件) group(分组) having(分组条件) order(排序) limit(限制)
	 * @example
	 * return array( 'example' => array( 'limit' => '0, 10' ));
	 * 调用方式：模型名::model()->example()->finds();
	 *
	 * @example，带参数形式的，在参数的，在数组里请用大括号包括自然数即可，从0开始。
	 * return array( 'example' => array( 'limit' => '{0}, {1}', 'order' => '{3}'));
	 * 调用方式：模型名::model()->example(5,10,'id DESC')->finds();
	 * 
	 * 数组里可以定义多个查询范围，在获取数据时可以随意组合使用。
	 *
	 * 当前ORM查询范围版本：1.0
	 * @return array
	 */
	public function scopes()
	{
		return array();
	}

	public function setScopes($func)
	{
		$this->scopesProcess = @array_merge($this->scopesProcess, $func());
		return $this;
	}

	/**
	 * 设置属性值
	 * @param string $name 	属性名
	 * @param void   $value 属性值
	 */
	public function __set($name,$value)
	{
		if (!$this->setAttribute($name,$value)) {
			dpException::error('该属性，所映射的字段不存在，'.get_class($this).'，'.$name);
		}
	}

	/**
	 * 获取属性值
	 * @param string $name 属性名
	 */
	public function __get($name)
	{
		if (isset($this->_attributes[$name])) return $this->_attributes[$name];
	}

	/**
	 * 魔术方法，调用查询范围或者关联查询方法时，调用该方法。
	 * 如果方法存在，则返回类本身，否则，抛出错误
	 * @return object 类本身
	 */
	public function __call($name, $params)
	{
		if (array_key_exists($name, $this->scopes())) {
			$scopes = $this->scopes();
			$this->scopesProcess = @array_merge(
				$this->scopesProcess, 
				$this->finder->process->parseScopes($scopes[$name], $params));
			return $this;
		} else if (array_key_exists($name, $this->relations())) {
			$rela = $this->relations();
			$this->with[$name] = $rela[$name];
			return $this;
		} else
			dpException::error('调用的方法不存在：'.$name.', '.get_Class($this));
	}

	/**
	 * 设置旧主键值
	 * @param int $pk 主键值
	 */
	public function setOldPk($pk)
	{
		$this->_pk = $pk;
	}

	/**
	 * 获取旧主键值
	 * @return int 主键值
	 */
	public function getOldPk()
	{
		return $this->_pk;
	}

	/**
	 * 设置查询的SQL语句
	 */
	protected function setSql($sql)
	{
		$this->_sql = $sql;
	}

	/**
	 * 返回最后查询的SQL语句
	 */
	public function getSql()
	{
		return $this->_sql;
	}

	/**
	 * insert或update前置操作，先执行该方法
	 * 前置操作必须返回true，才会执行下面，否则报错
	 * 模型继承dpActiveRecord后，重写该方法
	 */
	protected function beforeSave()
	{
		return true;
	}

	/**
	 * insert或update后置操作
	 * 在执行insert或update后触发该方法
	 * 模型继承dpActiveRecord后，重写该方法
	 */
	protected function afterSave(){}

	/**
	 * delete前置操作，先执行该方法
	 * 前置操作必须返回true，才会执行下面，否则报错
	 * 模型继承dpActiveRecord后，重写该方法
	 */
	protected function beforeDelete()
	{
		return true;
	}

	/**
	 * delete后置操作
	 * 在执行delete相关方法后触发该方法
	 * 模型继承dpActiveRecord后，重写该方法
	 */
	protected function afterDelete(){}

	/**
	 * find类方法前置操作，先执行该方法
	 * 前置操作必须返回true，才会执行下面，否则报错
	 * 模型继承dpActiveRecord后，重写该方法
	 */
	protected function beforeFind()
	{
		return true;
	}

	/**
	 * find类方法后置操作
	 * 在执行find类方法后触发该方法
	 * 模型继承dpActiveRecord后，重写该方法
	 */
	protected function afterFind(){}


	/**
	 * 以下是执行方法
	 */

	/**
	 * 按条件获取一条数据
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return array 结果集
	 */
	public function find($condition = '', $params = array())
	{
		if ($this->beforeFind()) {
			$record = $this->finder->getFind('', $condition, $params);
			$this->setSql($record['sql']);
			$this->afterFind();
			return $record['record'];
		} else
			return false;
	}

	/**
	 * 按主键获取一条数据
	 * @param int    $pk 	  primary key 主键ID
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return array 结果集
	 */
	public function findByPk($pk, $condition = '', $params = array())
	{
		if ($this->beforeFind()) {
			$record = $this->finder->getFind($pk, $condition, $params);
			$this->setSql($record['sql']);
			$this->afterFind();
			return $record['record'];
		} else
			return false;
	}

	/**
	 * 按属性(表字段)获取一条数据
	 * @param string or array $attributes 字段
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return array 结果集
	 */
	public function findByAttribute($attributes, $condition = '', $params = array())
	{
		if ($this->beforeFind()) {
			$record = $this->finder->getAttrFind($attributes, $condition, $params);
			$this->setSql($record['sql']);
			$this->afterFind();
			return $record['record'];
		} else
			return false;
	}

	/**
	 * 按SQL语句取出一条数据
	 * @param string $sql     SQL语句
	 * @param array  $params  条件参数绑定
	 * @return array 结果集
	 */
	public function findBySql($sql, $params = array())
	{
		if ($this->beforeFind()) {
			$record = $this->finder->getSqlFind($sql, $params);
			$this->setSql($record['sql']);
			$this->afterFind();
			return $record['record'];
		} else
			return false;
	}

	/**
	 * 按条件取出所有数据
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return array 结果集
	 */
	public function finds($condition = '', $params = array())
	{
		if ($this->beforeFind()) {
			$record = $this->finder->getFinds('', $condition, $params);
			$this->setSql($record['sql']);
			$this->afterFind();
			return $record['record'];
		} else
			return false;
	}

	/**
	 * 按主键取出带主键的所有数据
	 * @param int    $pk 	  primary key 主键ID
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return array 结果集
	 */
	public function findsByPk($pk, $condition = '', $params = array())
	{
		if ($this->beforeFind()) {
			$record = $this->finder->getFinds($pk, $condition, $params);
			$this->setSql($record['sql']);
			$this->afterFind();
			return $record['record'];
		} else
			return false;
	}

	/**
	 * 按属性(表字段)取出所有数据
	 * @param string or array $attributes 字段
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return array 结果集
	 */
	public function findsByAttribute($attributes, $condition = '', $params = array())
	{
		if ($this->beforeFind()) {
			$record = $this->finder->getAttrFinds($attributes, $condition, $params);
			$this->setSql($record['sql']);
			$this->afterFind();
			return $record['record'];
		} else
			return false;
	}

	/**
	 * 按SQL语句取出所有数据
	 * @param string $sql     SQL语句
	 * @param array  $params  条件参数绑定
	 * @return array 结果集
	 */
	public function findsBySql($sql, $params = array())
	{
		if ($this->beforeFind()) {
			$record = $this->finder->getSqlFinds($sql, $params);
			$this->setSql($record['sql']);
			$this->afterFind();
			return $record['record'];
		} else
			return false;
	}

	/**
	 * 获取find类操作后的记录数
	 * @return int 记录数
	 */
	public function count()
	{
		return $this->finder->getCount();
	}

	/**
	 * 按SQL语句获取记录数
	 * @param string $sql    SQL语句
	 * @return int 记录数
	 */
	public function countBySql($sql)
	{
		return $this->finder->getCount($sql);
	}

	/**
	 * 按属性更新满足条件的所有数据
	 * @param array  $attributes 字段
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return int or boolean 成功返回影响行数，否则返回false
	 */
	public function updates($attributes = array(), $condition = '', $params = array())
	{
		if ($this->beforeSave()) {
			$attributes    = empty($attributes) ? $this->getData() : $attributes;
			if(empty($attributes)) return false;
			$data['attr']  = array_intersect_key($attributes, array_flip($this->getColumnName()));
			$data['attrs'] = $attributes;
			$affrow = $this->finder->getUpdate('', $data, $condition, $params);
			$this->afterSave();

			if($affrow == 0)
				return true;
			else
				return $affrow;
		} else
			return false;
	}

	/**
	 * 更新满足主键和条件的数据
	 * @param int    $pk 	  primary key 主键ID
	 * @param array  $attributes 字段
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return int or boolean 成功返回影响行数，否则返回false
	 */
	public function updateByPk($pk, $attributes = array(), $condition = '', $params = array())
	{
		if ($this->beforeSave()) {
			$attributes    = empty($attributes) ? $this->getData() : $attributes;

			if(empty($attributes)) return false;
			$data['attr']  = array_intersect_key($attributes, array_flip($this->getColumnName()));
			$data['attrs'] = $attributes;
			$affrow = $this->finder->getUpdate($pk, $data, $condition, $params);
			$this->{$this->primaryKey()} = $pk;
			$this->afterSave();

			if ($affrow == 0)
				return true;
			else
				return $affrow;
		} else
			return false;
	}

	/**
	 * 删除满足条件的所有数据
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return int or boolean 成功返回影响行数，否则返回false
	 */
	public function deletes($condition = '', $params = array())
	{
		if ($this->beforeDelete()) {
			$affrow = $this->finder->getDelete('', $condition, $params);
			$this->afterDelete();

			if ($affrow == 0)
				return true;
			else
				return $affrow;
		} else
			return false;
	}

	/**
	 * 删除满足主键和条件的数据
	 * @param int    $pk 	  primary key 主键ID
	 * @param string or array $condition SQL条件
	 * @param array  $params  条件参数绑定
	 * @return int or boolean 成功返回影响行数，否则返回false
	 */
	public function deleteByPk($pk, $condition = '', $params = array())
	{
		if ($this->beforeDelete()) {
			$this->setAttribute($this->primaryKey(), null);
			$affrow = $this->finder->getDelete($pk, $condition, $params);
			$this->afterDelete();

			if ($affrow == 0)
				return true;
			else
				return $affrow;
		} else
			return false;
	}

	/**
	 * 添加一条数据
	 * @return boolean
	 */
	public function insert($attributes = array())
	{
		if (!$this->getIsNew())
			dpException::error('Active Record 无法新增记录，因为不是新的对象实例');

		if ($this->beforeSave()) {
			$attributes    = empty($attributes) ? $this->getData() : $attributes;

			if(empty($attributes)) return false;

			$data['attr']  = array_intersect_key($attributes, array_flip($this->getColumnName()));
			$data['attrs'] = $attributes;
			$this->setOldPk($this->{$this->primaryKey()});
			$pk = $this->finder->getInsert($data);
			if ($this->primaryKey()) {
				$this->{$this->primaryKey()} = $pk;
			}
			$this->setIsNew(false);
			$this->afterSave();
			return $this->primaryKey() ? $pk : true;
		} else return false;
	}

	/**
	 * 特殊方法，添加数据或者更新数据
	 * @return boolean
	 */
	public function save($attributes = null)
	{
		return $this->getIsNew() ? $this->insert($attributes) :
			   empty($this->{$this->primaryKey()}) ? false : 
			   $this->updateByPk($this->{$this->primaryKey()},$attributes);
	}

	/**
	 * 执行方法结束
	 */

}


?>